/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2010 huliqing, huliqing.cn@gmail.com
 *
 * This file is part of QFaces.
 * QFaces is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * QFaces is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with QFaces.  If not, see <http://www.gnu.org/licenses/>.
 *
 * 这个文件是QFaces的一部分。
 * 您可以单独使用或分发这个文件，但请不要移除这个头部声明信息.
 * QFaces是一个自由软件，您可以自由分发、修改其中的源代码或者重新发布它，
 * 新的任何修改后的重新发布版必须同样在遵守LGPL3或更后续的版本协议下发布.
 * 关于LGPL协议的细则请参考COPYING、COPYING.LESSER文件，
 * 您可以在QFaces的相关目录中获得LGPL协议的副本，
 * 如果没有找到，请连接到 http://www.gnu.org/licenses/ 查看并获取副本。
 *
 * - Author: Huliqing
 * - Contact: huliqing.cn@gmail.com
 * - License: GNU Lesser General Public License (LGPL)
 * - Blog and source code availability: http://www.huliqing.name/
 */

package name.huliqing.qfaces.component;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Map;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.el.MethodExpression;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.imageio.ImageIO;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import name.huliqing.qfaces.QFaces;
import name.huliqing.qfaces.model.ValidateCode;

public class GraphValidator extends UIInput implements Ajax, Filter {
	private final static Logger logger = Logger.getLogger(GraphValidator.class.getName());

	// 生成的图形的宽度及高度
	private int width;
	private int height;
	
	// 使用的字体名称
	private String fontName;
	
	// 字体文件路径,即直接指定使用某个字体文件的路径
	private String fontPath;
	
	// 默认的字体大小
	private Integer fontSize;
	
	// 字体左上角的X偏移量
	private Integer offsetX;
	
	// 字体左上角的Y偏移量
	private Integer offsetY;
	
	// 字符间的水平间隔
	private Integer space;
	
	// 显示的信息
	private String message;
	
	// 验证正确时的message
	private String messageCorrect;
	
	// 验证错误时的message
	private String messageError;
	
	// 是否使用lazy模式, 如果为true,则只有在鼠标放到(focus)input框时才生成图形,
	// 否则在组件载入时就生成图形
	private Boolean lazy;
	
	// 验证时是否不区分大小写
	private Boolean ignoreCase;
	
	// 是否将图片渲染在input框的上面(默认为下面)
	private Boolean renderOnTop;
	
	// 是否显示“换下一张”按钮
	private Boolean renderNextButton;
	
	// 关于"下一张"的标签描述文字
	private String labelNext;
	
	// 是否关闭ajax验证,如果关闭该功能，则不进行ajax验证。
	// 即只在表单提交时才进行验证。
	private Boolean disableAjax;
	
	// 图形的最大扭曲度
	private Integer distort;
	
	// 杂斑数量
	private Integer diverse;
	
	// 侦听器，该方法绑定允许由用户自己创建图形文件.绑定的方法签名如：
	// public BufferedImage renderListener(ValidateCode vCode);
	// 即给定一个ValidateCode， 然后由用户自己产生图形，并返回一个BufferedImage
	private MethodExpression renderListener;
	
	// message默认样式
	private String styleClass;

	// Input框的样式
	private String styleClassInput;
	
	// 验证正确时的样式
	private String styleClassCorrect;
	
	// 验证错误时的样式
	private String styleClassError;
	
	// ---- 内部参数
	
	// 组件ID标识
	private String ID_CLIENT;
	
	// 整个渲染区域的ID
	private String ID_FULL;
	
	// 存放输入值的Field
	private String ID_INPUT;
	
	// 存放图形区域的ID
	private String ID_GRAPH_ZONE;
	
	// Message信息区域的ID，该区域存放验证信息
	private String ID_MESSAGE_ZONE;
	
	// 关于换一张(下一张)图片
	private String ID_NEXT;
	
	// ValidateCode的标识ID
	private String ID_VALIDATE_CODE;
	
	// 表示验证结果是否正确
	// 该组件的当前状态,定义如下:
	// -1	-> 发生验证错误的
	// 0	-> 状态未知的，也就是未经过验证的
	// 1	-> 经过验证为正确的
	private Integer state;
	
	@Override
	public void restoreState(FacesContext fc, Object state) {
		Object[] objs = (Object[]) state;
		super.restoreState(fc, objs[0]);
		this.messageError = (String) objs[1]; 		// use in "validate"
		this.ignoreCase = (Boolean) objs[2]; 		// use in "validate"
		this.ID_INPUT = (String) objs[3]; 			// use in "decode"
		this.ID_VALIDATE_CODE = (String) objs[4]; 	// use in "validate"
		this.renderListener = (MethodExpression) objs[5];
	}

	@Override
	public Object saveState(FacesContext fc) {
		Object[] objs = new Object[6];
		objs[0] = super.saveState(fc);
		objs[1] = this.messageError;
		objs[2] = this.ignoreCase;
		objs[3] = this.ID_INPUT;
		objs[4] = this.ID_VALIDATE_CODE;
		objs[5] = this.renderListener;
		return objs;
	}
	
	public void setRenderListener(MethodExpression renderListener) {
		this.renderListener = renderListener;
	}

	@Override
	public void encodeChildren(FacesContext fc) throws IOException {
		super.encodeChildren(fc);
	}

	@Override
    public void encodeBegin(FacesContext fc) throws IOException {
		encodeGraph(fc);		
	}
	
	@Override
	public void encodeEnd(FacesContext fc) throws IOException {}
	
    @Override
	public void decode(FacesContext fc) {
		super.decode(fc);
//		// 取得input中的值，并重置submittedValue，否则 encodeGraph中getValue会拿不到值
//		String inputValue = QFaces.getParam(this.ID_INPUT);
//		this.setSubmittedValue(inputValue);
	}

	private void encodeGraph(FacesContext fc) throws IOException {
		QFaces.getResLoader2().loadJavaScript(this, "GraphValidator.js");
		// ---- 初始化参数
		this.width = QFaces.convertToInteger(QFaces.getAttribute(this, "width", 156));
		this.height = QFaces.convertToInteger(QFaces.getAttribute(this, "height", 1));
		this.fontName = (String) QFaces.getAttribute(this, "fontName", "Times New Roman");
		this.fontPath = (String) QFaces.getAttribute(this, "fontPath", "");
		this.fontSize = QFaces.convertToInteger(QFaces.getAttribute(this, "fontSize", 38));
		this.offsetX = QFaces.convertToInteger(QFaces.getAttribute(this, "offsetX", 10));
		this.offsetY = QFaces.convertToInteger(QFaces.getAttribute(this, "offsetY", 0));
		this.space = QFaces.convertToInteger(QFaces.getAttribute(this, "space", 9));
		this.message = (String) (QFaces.getAttribute(this, "message", "Validate Code"));
		this.messageCorrect = (String) (QFaces.getAttribute(this, "messageCorrect", message)); // use "message" for defaultValue
		this.messageError = (String) (QFaces.getAttribute(this, "messageError", message));
		this.lazy = QFaces.convertToBoolean(QFaces.getAttribute(this, "lazy", false));
		this.ignoreCase = QFaces.convertToBoolean(QFaces.getAttribute(this, "ignoreCase", true));
		this.renderOnTop = QFaces.convertToBoolean(QFaces.getAttribute(this, "renderOnTop", false));
		this.renderNextButton = QFaces.convertToBoolean(QFaces.getAttribute(this, "renderNextButton", true));
		this.labelNext = (String) QFaces.getAttribute(this, "labelNext", "看不清，换下一张？");
		this.disableAjax = QFaces.convertToBoolean(QFaces.getAttribute(this, "disableAjax", false));
		this.distort = QFaces.convertToInteger(QFaces.getAttribute(this, "distort", 4));
		this.diverse = QFaces.convertToInteger(QFaces.getAttribute(this, "diverse", 150));
		
		this.styleClass = (String) QFaces.getAttribute(this, "styleClass", "_qf_06_message");
		this.styleClassInput = (String) QFaces.getAttribute(this, "styleClassInput", "_qf_06_input");
		this.styleClassCorrect = (String) QFaces.getAttribute(this, "styleClassCorrect", "_qf_06_message");
		this.styleClassError = (String) QFaces.getAttribute(this, "styleClassError", "_qf_06_error");
		
		if (this.width <= 0)
			this.width = 1;
		if (this.height <= 0)
			this.height = 1;
		if (this.state == null)
			this.state = 0;
		
		// ---- 设置ID属性
		this.ID_CLIENT = getClientId(fc);
		this.ID_FULL = ID_CLIENT + ":full";
		this.ID_INPUT = ID_CLIENT;
		this.ID_GRAPH_ZONE = ID_CLIENT + ":graph";
		this.ID_MESSAGE_ZONE = ID_CLIENT + ":message";
		this.ID_NEXT = ID_CLIENT + ":next";
		// 通过viewId与组件clientId组合成一个验证码ID，以保证ID为唯一
		this.ID_VALIDATE_CODE = fc.getViewRoot().getViewId() + ":" + ID_CLIENT;
		
		// ---- 开始渲染
		ResponseWriter rw = fc.getResponseWriter();
		// Render input field
		rw.startElement("div", this);
		rw.writeAttribute("id", ID_FULL, null);
		rw.writeAttribute("style", "border:0;", null);
		
			if (this.renderOnTop) {
				// Render "image" zone on top
				rw.startElement("div", this);
				rw.writeAttribute("id", ID_GRAPH_ZONE, null);
				rw.writeAttribute("style", "border:0;", null);
				rw.endElement("div");
			}
		
			// Render "input" zone
			rw.startElement("input", this);
			rw.writeAttribute("id", ID_INPUT, null);
			rw.writeAttribute("name", ID_INPUT, null);
			rw.writeAttribute("class", styleClassInput, null);
			rw.writeAttribute("type", "text", null);
			rw.writeAttribute("value", (this.getValue() != null ? this.getValue() : ""), null);
			rw.endElement("input");
			
			// Render "message" zone
			rw.startElement("span", this);
			rw.writeAttribute("id", ID_MESSAGE_ZONE, null);
			rw.writeAttribute("style", "border:0;", null);
			rw.endElement("span");
			
			if (!this.renderOnTop) {
				// Render "image" zone on bottom
				rw.startElement("div", this);
				rw.writeAttribute("id", ID_GRAPH_ZONE, null);
				rw.writeAttribute("style", "border:0;", null);
				rw.endElement("div");				
			}
			
			// Render "next" zone
			if (this.renderNextButton) {
				rw.startElement("div", this);
				rw.writeAttribute("style", "border:0;", null);
				rw.startElement("a", this);
				rw.writeAttribute("id", ID_NEXT, null);
				rw.writeAttribute("href", "#", null);
				rw.writeAttribute("tabindex", "-1", null);
				rw.writeAttribute("style", "cursor:pointer;border:0;", null);
				rw.writeText(this.labelNext, null);
				rw.endElement("a");
				rw.endElement("div");
			}
		rw.endElement("div");
		
		StringBuilder sb = new StringBuilder("Q.F.UIGraphValidator.init(")
		.append(" \"" + this.ID_CLIENT + "\"")
		.append(",\"" + this.ID_FULL + "\"")
		.append(",\"" + this.ID_INPUT + "\"")
		.append(",\"" + this.ID_GRAPH_ZONE + "\"")
		.append(",\"" + this.ID_MESSAGE_ZONE + "\"")
		.append(",\"" + this.ID_NEXT + "\"")
		.append(",\"" + this.ID_VALIDATE_CODE + "\"")
		.append(",\"" + this.state + "\"")
		.append(",\"" + this.getClass().getName() + "\"")
		.append(",\"" + this.width + "\"")
		.append(",\"" + this.height + "\"")
		.append(",\"" + this.fontName + "\"")
		.append(",\"" + this.fontPath + "\"")
		.append(",\"" + this.fontSize + "\"")
		.append(",\"" + this.offsetX + "\"")
		.append(",\"" + this.offsetY + "\"")
		.append(",\"" + this.space + "\"")
		.append(",\"" + this.message + "\"")
		.append(",\"" + this.messageCorrect + "\"")
		.append(",\"" + this.messageError + "\"")
		.append(",\"" + this.lazy.toString() + "\"")
		.append(",\"" + this.ignoreCase.toString() + "\"")
		.append(",\"" + this.renderNextButton.toString() + "\"")
		.append(",\"" + this.disableAjax.toString() + "\"")
		.append(",\"" + this.distort + "\"")
		.append(",\"" + this.diverse + "\"")
		.append(",\"" + (this.renderListener != null ? renderListener.getExpressionString() : "") + "\"")
		.append(",\"" + this.styleClass + "\"")
		.append(",\"" + this.styleClassCorrect + "\"")
		.append(",\"" + this.styleClassError + "\"")
		.append(")");
		QFaces.getResLoader2().loadJavaScriptInit(this, sb.toString());
	}
	

	@Override
	public void validate(FacesContext fc) {
		super.validate(fc);
		String _code = "";
		Object temp = this.getValue();
		if (temp != null)
			_code = temp.toString();
		boolean valid = GraphValidatorManager.validate(ID_VALIDATE_CODE, _code, this.ignoreCase);
		if (valid) {
			this.state = 1;
		} else {
			this.state = -1;
			// 额外的支持
			FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, this.messageError, this.messageError);
			fc.addMessage(getClientId(fc), message);
			// 直接跳到最后阶段
			this.setValid(false);
			fc.renderResponse();
		}
	}

	public void ajax(Map<String, String> params, FacesContext fc) {
		String _validateCodeId = params.get("validateCodeId");
		String _code = params.get("code");
		Boolean _ignoreCase = Boolean.parseBoolean(params.get("ignoreCase"));
		boolean valid = GraphValidatorManager.validate(_validateCodeId, _code, _ignoreCase);
		QFaces.getResponse().responseHTML(fc, (valid ? "true" : "false"));
	}

	/**
	 * 生成图形
	 */
	public void filter(Map<String, String> params, FacesContext fc) {
		// 获取参数
		String _validateCodeId = params.get("validateCodeId");
		String _renderListenerExp = params.get("renderListenerExp");
		String _viewId = params.get("viewId");
		Integer _width = Integer.parseInt(params.get("width"));
		Integer _height = Integer.parseInt(params.get("height"));		
		String _fontName = params.get("fontName");
		String _fontPath = params.get("fontPath");
		Integer _fontSize = Integer.parseInt(params.get("fontSize"));
		Integer _offsetX = Integer.parseInt(params.get("offsetX"));
		Integer _offsetY = Integer.parseInt(params.get("offsetY"));
		Integer _distort = Integer.parseInt(params.get("distort"));
		Integer _diverse = Integer.parseInt(params.get("diverse"));
		Integer _space = Integer.parseInt(params.get("space"));
		Boolean _create = Boolean.parseBoolean(params.get("create"));
		
		// 生成验证码, 如果Create为true,则重新创建一个;
		// 否则寻找原始validateCode(如果找不到，则再创建一个)
		ValidateCode vCode = null;
		if (!_create)
			vCode = GraphValidatorManager.findValidateCode(_validateCodeId);
		if (vCode == null)
			vCode = GraphValidatorManager.createValidateCode(_validateCodeId);
		char[] chars = vCode.getCode().toCharArray();
		
		// 开始生成图片
		HttpServletResponse response = (HttpServletResponse) fc.getExternalContext().getResponse();
		response.setHeader("Cache-Control", "no-cache");
		response.setCharacterEncoding("utf-8");
		response.setContentType("image/jpeg");
		BufferedImage buff = null;
		
		// 如果由用户自己生成图形
		if (_renderListenerExp != null && _renderListenerExp.length() > 0) {
			MethodExpression me = QFaces.createMethodExpression(_renderListenerExp, BufferedImage.class, new Class[]{ValidateCode.class});
			buff = (BufferedImage) me.invoke(fc.getELContext(), new Object[]{vCode});
		} else {
			// 生成字体
			Font font = null;
			if (_fontPath != null && !_fontPath.equals("")) {
				String fullPath = null;
				ServletContext sc = (ServletContext)fc.getExternalContext().getContext();
				if (_fontPath.startsWith("/")) {
					fullPath = sc.getRealPath("/") + _fontPath;
				} else {
					try {
						String tempPath = sc.getResource(_viewId).getPath();
						fullPath = tempPath.substring(0, tempPath.lastIndexOf("/") + 1) + _fontPath;
					} catch (MalformedURLException mu) {
						logger.log(Level.SEVERE, mu.getMessage(), mu);
					}
				}
				try {
					font = Font.createFont(Font.TRUETYPE_FONT, new File(fullPath));
					font = font.deriveFont(Font.BOLD, _fontSize);
				} catch (IOException ioe) {
					logger.log(Level.SEVERE, ioe.getMessage(), ioe);
				} catch (FontFormatException ffe) {				
					logger.log(Level.SEVERE, ffe.getMessage(), ffe);
				}
			}
			if (font == null)
				font = new Font(_fontName, Font.BOLD, _fontSize);
			
			// 生成默认的图形
			buff = new BufferedImage(_width, _height, BufferedImage.TYPE_INT_RGB);
			Graphics2D g2 = (Graphics2D) buff.getGraphics();
			
			// 设置字体
			g2.setFont(font);
			FontMetrics fm = g2.getFontMetrics(font);
			
			// 计算字符所需的最少宽度及高度(尽量保证字体居中)
			int needWidth = fm.charsWidth(chars, 0, chars.length) + (_offsetX * 2) + (_space * chars.length - 1);
			int needHeight = _offsetY + fm.getHeight();
			if (_width < needWidth || _height < needHeight) {
				if (_width < needWidth)
					_width = needWidth;
				if (_height < needHeight)
					_height = needHeight;
				buff = new BufferedImage(_width, _height, BufferedImage.TYPE_INT_RGB);
				g2 = (Graphics2D) buff.getGraphics();
				g2.setFont(font);
			}
			
			// Draw background
			g2.setColor(Color.WHITE);
			g2.fillRect(0, 0, _width, _height);
			
			// Draw border
			g2.setColor(Color.GRAY);
			g2.drawRect(0, 0, _width - 1, _height - 1);
			
			// Draw line
			Random random = new Random();
			for (int i = 0; i < _diverse; i++) {
				g2.setColor(new Color((float)Math.random(), (float)Math.random(), (float)Math.random()));
				int x1 = random.nextInt(_width);
			    int y1 = random.nextInt(_height);
			    int x2 = random.nextInt(8);
			    int y2 = random.nextInt(8);
			    g2.drawLine(x1, y1, x1+x2, y1+y2);
			}
			
			// Draw String
			int baselineX = _offsetX;
			int baselineY = _offsetY + fm.getAscent() - fm.getLeading();
			for (int i = 0; i < chars.length; i++) {
				g2.setColor(new Color((float)Math.random(), (float)Math.random(), (float)Math.random()));
				g2.drawChars(chars, i, 1, baselineX, baselineY);
				baselineX += fm.charWidth(chars[i]) + _space;
			}
			
			// 至少保证扭曲度为2
			_distort = (int) (_distort * Math.random());
			_distort += 2;
			if (_distort > 0) {
				WritableRaster wr = buff.getRaster();
				int[] pix = new int[3];
				int max = 8; // max distort
				int ox = 0; // offsetX
				double mh = _height / _distort; // mh -> middle height;
				int rise = 1;
				for (int i = _width - 2; i >= 0; i--) { // _width - 2,避免border被擦除
					for (int j = 0, s = 0; j < _height; j++) {
						if (s > mh || s <= 0)
							rise *= -1;
						s += rise;
						if (s < 0)
							s = 0;
						ox = (int) ((s / mh) * max);
						pix = wr.getPixel((i - ox >= 0 ? i - ox : 0), j, pix);
						if (pix[0] != 128 || pix[1] != 128 || pix[2] != 128)
							wr.setPixel(i, j, pix);
					}
				}				
			}
			g2.dispose();
		}
		try {
			ImageIO.write(buff, "JPEG", response.getOutputStream());
		} catch (IOException ioe) {
			logger.log(Level.SEVERE, ioe.getMessage(), ioe);
		}
	}
	
}
